[#_lesson_29_keeping_data] == Leçon 29 : Garder des données image::learn/29-databases.png[La leçon terminée, 480] La leçon douze sauvegardait un meilleur score : un nombre, sous un nom, toujours là le lendemain matin. C'est exactement ce qu'il faut pour un meilleur score, et ce qu'il ne faut pas pour presque tout le reste de ce qu'un jeu veut garder. Une liste de toutes les parties que quelqu'un a jouées. Le meilleur temps sur chacun de vingt circuits. Quarante niveaux que quelqu'un a conçus dans un tableur. La question « quel est mon meilleur score sur l'épreuve difficile ? », à laquelle une pile de nombres sauvegardés ne peut pas répondre sans que vous écriviez la réponse à la main. C'est à ça que sert une base de données, et Singe en a une intégrée. Cette leçon reprend le test de réaction de la leçon vingt-huit, lui donne des épreuves livrées avec le jeu, et lui fait garder chaque partie jouée par qui que ce soit et répondre à des questions à leur sujet. [#_what_a_database_is] === Ce qu'est une base de données Une base de données est un fichier de *tables*. Une table, ce sont des lignes et des colonnes, comme un tableur dont les noms de colonnes sont décidés à l'avance : une table de parties pourrait avoir une épreuve, un score et une heure pour chaque ligne. Vous y mettez des lignes et vous posez des questions pour en faire sortir, et les questions s'écrivent en *SQL*, un petit langage qui fait ce travail depuis avant la plupart des langages de programmation dont vous avez entendu parler. La base de données que Singe emporte avec lui est *SQLite*. Ce n'est pas un serveur qu'on installe ni un service auquel on s'inscrit. C'est une bibliothèque, la base de données est un fichier ordinaire, et le même SQLite se trouve dans votre téléphone, dans votre navigateur et probablement dans votre voiture. Vous n'avez pas besoin de connaître le SQL pour suivre cette leçon ; chaque instruction y est expliquée au passage, et les trois dont vous vous servirez le plus -- `CREATE TABLE`, `INSERT` et `SELECT` -- sont toutes là. [#_opening_one] === En ouvrir une [source,lua] ---- local sqlite3 = require("sqlite3") local runs = sqlite3.open(singeGetDataPath() .. "runs.db") ---- `require("sqlite3")` charge une bibliothèque fournie avec Singe et vous la rend sous forme de table, si bien que tout ce que SQLite sait faire s'écrit `sqlite3.` quelque chose. `sqlite3.open` ouvre un fichier de base de données et le crée s'il n'existe pas encore : le premier lancement de votre jeu en crée un vide, et chaque lancement suivant le retrouve. Le fichier va dans `singeGetDataPath()`, pour la même raison que la sauvegarde de la leçon douze : le dossier d'un jeu peut se trouver sur un disque, sur un partage réseau, ou être empaqueté dans un seul fichier, et on ne peut écrire dans aucun d'eux. Tout ce qu'un jeu écrit va dans son dossier de données. [#_making_a_table] === Créer une table Une base de données vide n'a aucune table. Vous en créez une en la décrivant : [source,lua] ---- runs:exec([[ CREATE TABLE IF NOT EXISTS runs ( id INTEGER PRIMARY KEY, stage TEXT NOT NULL, score INTEGER NOT NULL, at INTEGER NOT NULL ) ]]) ---- `exec` exécute du SQL qui ne renvoie rien. Les doubles crochets sont la chaîne longue de Lua : tout ce qui se trouve entre eux, retours à la ligne compris, forme une seule chaîne, et c'est comme ça qu'on garde le SQL lisible. `CREATE TABLE` nomme la table et ses colonnes, chacune avec un type : `TEXT` pour les mots et `INTEGER` pour les nombres entiers. `NOT NULL` veut dire que chaque ligne doit avoir une valeur à cet endroit. `INTEGER PRIMARY KEY` est spécial : SQLite le remplit tout seul, en comptant, si bien que chaque ligne reçoit son propre numéro sans que vous ayez à y penser. `IF NOT EXISTS` est ce qui permet de l'exécuter sans risque à chaque démarrage du jeu. La première fois, il crée la table. Toutes les fois suivantes, la table est déjà là et la ligne ne fait rien. Sans ces trois mots, le deuxième lancement de votre jeu s'arrêterait sur une erreur disant que la table existe déjà. [#_putting_a_row_in] === Ajouter une ligne Vous pourriez construire le SQL d'une nouvelle ligne en collant des chaînes bout à bout : [source,lua] ---- runs:exec("INSERT INTO runs (stage, score, at) VALUES ('" .. stage .. "', " .. score .. ", " .. os.time() .. ")") ---- Ne le faites pas. Ça marche jusqu'au jour où un nom contient une apostrophe : l'apostrophe termine alors la chaîne trop tôt et l'instruction n'a plus aucun sens. Dans un jeu qui laisse les joueurs taper, c'est pire : tout ce qu'ils tapent devient un morceau de votre SQL. La bonne manière consiste à écrire l'instruction une fois, avec des points d'interrogation à la place des valeurs, et à passer les valeurs à part : [source,lua] ---- local addRun = runs:prepare("INSERT INTO runs (stage, score, at) VALUES (?, ?, ?)") local function record(stage, points) addRun:bind_values(stage, points, os.time()) addRun:step() addRun:reset() end ---- `prepare` transforme le SQL en une *instruction* qu'on peut utiliser encore et encore. `bind_values` met une valeur dans chaque point d'interrogation, dans l'ordre. Quoi qu'une valeur contienne, apostrophes comprises, c'est une valeur et jamais un morceau du SQL. `step` exécute l'instruction, et `reset` la remet en état d'être exécutée de nouveau. `os.time()` est du Lua tout simple : l'heure actuelle sous forme de nombre de secondes, ce qui est la façon la plus facile de stocker « quand ». Préparer une fois et réutiliser l'instruction est aussi plus rapide, parce que SQLite ne calcule qu'une seule fois comment l'exécuter. Pour un jeu qui écrit une ligne par minute, ça ne compte pas. Pour un éditeur de niveaux qui en écrit mille, si. [#_asking_questions] === Poser des questions Les lignes ressortent avec `nrows`, qui exécute un `SELECT` et vous rend chaque ligne sous forme de table, avec les noms des colonnes comme clés : [source,lua] ---- for row in runs:nrows("SELECT stage, score FROM runs ORDER BY score DESC LIMIT 5") do debugPrint(row.stage .. " " .. row.score) end ---- Lisez le SQL de gauche à droite. `SELECT stage, score` dit quelles colonnes vous voulez. `FROM runs` dit quelle table. `ORDER BY score DESC` trie les lignes par score, le plus haut d'abord, et `LIMIT 5` s'arrête après cinq. C'est un tableau des meilleurs scores, et il n'a jamais eu besoin de code de tri de votre part. Les questions peuvent être plus astucieuses que celles que vous auriez envie d'écrire à la main : [source,lua] ---- for row in runs:nrows("SELECT stage, MAX(score) AS top, COUNT(*) AS tries FROM runs GROUP BY stage ORDER BY stage") do debugPrint(row.stage .. ": best " .. row.top .. " of " .. row.tries) end ---- `GROUP BY stage` rassemble les lignes en un groupe par épreuve, et `MAX(score)` et `COUNT(*)` sont calculés pour chaque groupe : le meilleur score sur cette épreuve et le nombre de fois qu'elle a été jouée. `AS top` donne un nom à la réponse, et c'est la clé avec laquelle vous la relisez. Quand une question n'a qu'une réponse, `urows` vous rend les colonnes elles-mêmes plutôt qu'une table, ce qui est plus net pour un comptage : [source,lua] ---- for count in runs:urows("SELECT COUNT(*) FROM runs") do played = count end ---- [#_ask_when_it_changes] === Demandez quand ça change Aucune de ces questions n'a sa place dans `onOverlayUpdate`. Une requête est rapide, mais soixante par seconde, pour obtenir des réponses qui changent une fois par minute, c'est le jeu qui pose la même question et obtient la même réponse cinquante-neuf fois sur soixante. Alors le jeu demande quand la réponse a pu changer -- une fois au début, et de nouveau chaque fois qu'une partie est enregistrée -- et garde ce qui est revenu dans des tables ordinaires : [source,lua] ---- local function refresh() best = {} for row in runs:nrows("SELECT stage, score FROM runs ORDER BY score DESC, id LIMIT 5") do best[#best + 1] = row end -- et pareil pour les deux autres questions end ---- `onOverlayUpdate` dessine à partir de `best`, ce qui ne coûte rien. `record` appelle `refresh` après chaque nouvelle ligne, donc le tableau à l'écran n'est jamais périmé. [#_data_that_ships_with_the_game] === Les données livrées avec le jeu L'autre moitié des données d'un jeu, c'est la partie que vous écrivez avant que quiconque y joue : les épreuves, les niveaux, les objets, les répliques des dialogues. Ça aussi, ça peut être une base de données, créée avec n'importe quel outil SQLite -- le programme `sqlite3` en ligne de commande, ou un programme avec des fenêtres comme DB Browser for SQLite -- et rangée dans le dossier de votre jeu à côté de ses images. Le kit graphique en a une, `stages.db`, avec un nom et l'attente la plus courte et la plus longue de chaque épreuve. Un jeu ouvre celle-là autrement : [source,lua] ---- local stages = {} local shipped = sqlite3.open(DIR .. "art/stages.db", sqlite3.OPEN_READONLY) for row in shipped:nrows("SELECT name, waitMin, waitMax FROM stages ORDER BY id") do stages[#stages + 1] = row end shipped:close() ---- `DIR` est le dossier de votre jeu, là où sont les images. `sqlite3.OPEN_READONLY` dit que vous comptez seulement la lire, et ça compte pour le jour où vous publierez. Quand vous empaquetterez votre jeu dans un seul fichier à la leçon suivante, une base de données ouverte en lecture seule sera lue directement dans ce fichier, sans rien dépaqueter. Une base ouverte en écriture est copiée, une fois, dans le dossier de données du jeu, et c'est la copie que vous modifiez à partir de là ; le fichier que vous avez publié n'est jamais écrit. Alors gardez les deux sortes à part. Ce que vous avez fait va dans votre dossier et s'ouvre en lecture seule. Ce que le jeu fait va dans le dossier de données. Ici, les épreuves sont lues une seule fois au début dans une table ordinaire et la base de données est fermée, parce qu'une liste de quatre épreuves ne vaut pas la peine de garder un fichier ouvert. [#_closing_up] === Tout fermer Les instructions et les bases de données gardent de la mémoire et un fichier ouvert jusqu'à ce que vous disiez que vous avez fini, et l'endroit propre pour le dire, c'est `onShutdown` : [source,lua] ---- function onShutdown() addRun:finalize() runs:close() end ---- `finalize`, c'est le `close` d'une instruction. Une base de données ne se ferme pas tant qu'une instruction créée à partir d'elle existe encore, donc les instructions passent en premier. [#_the_whole_script_4] === Le script complet Créez un dossier appelé `runs`, copiez-y le dossier `art` du livre pour `stages.db`, et mettez ceci dans `runs.singe` : [source,lua] ---- dofile("Singe/Framework.singe") local sqlite3 = require("sqlite3") local PERFECT = 1000 -- Ce qui est livré avec le jeu : les épreuves, lues une fois puis fermées. local stages = {} local shipped = sqlite3.open(DIR .. "art/stages.db", sqlite3.OPEN_READONLY) for row in shipped:nrows("SELECT name, waitMin, waitMax FROM stages ORDER BY id") do stages[#stages + 1] = row end shipped:close() -- Ce que le jeu écrit : chaque partie jouée, gardée dans le dossier de données. local runs = sqlite3.open(singeGetDataPath() .. "runs.db") runs:exec([[ CREATE TABLE IF NOT EXISTS runs ( id INTEGER PRIMARY KEY, stage TEXT NOT NULL, score INTEGER NOT NULL, at INTEGER NOT NULL ) ]]) local addRun = runs:prepare("INSERT INTO runs (stage, score, at) VALUES (?, ?, ?)") local stageAt = 1 local state = "attract" local goAt = 0 local score = 0 local best = {} local perStage = {} local played = 0 -- Les questions, posées quand une partie est ajoutée plutôt qu'à chaque image. local function refresh() best = {} for row in runs:nrows("SELECT stage, score FROM runs ORDER BY score DESC, id LIMIT 5") do best[#best + 1] = row end perStage = {} for row in runs:nrows("SELECT stage, MAX(score) AS top, COUNT(*) AS tries FROM runs GROUP BY stage ORDER BY stage") do perStage[#perStage + 1] = row end for count in runs:urows("SELECT COUNT(*) FROM runs") do played = count end end local function record(stage, points) addRun:bind_values(stage, points, os.time()) addRun:step() addRun:reset() refresh() end local function play() local stage = stages[stageAt] state = "waiting" goAt = singeGetTicks() + math.random(stage.waitMin, stage.waitMax) score = 0 end function onInputPressed(what) if what == SWITCH_BUTTON2 and state ~= "waiting" and state ~= "go" then stageAt = stageAt % #stages + 1 return end if what ~= SWITCH_BUTTON1 then return end if state == "waiting" then state = "result" score = 0 record(stages[stageAt].name, score) elseif state == "go" then score = math.max(0, PERFECT - (singeGetTicks() - goAt)) state = "result" record(stages[stageAt].name, score) else play() end end function onOverlayUpdate() if state == "waiting" and singeGetTicks() >= goAt then state = "go" end overlayClear() overlayPrint(2, 0, "REACTION TEST: " .. stages[stageAt].name) if state == "attract" then overlayPrint(2, 2, "Button 1 to play, button 2 for another stage.") elseif state == "waiting" then overlayPrint(2, 2, "Wait for it...") elseif state == "go" then overlayPrint(2, 2, "NOW!") else overlayPrint(2, 2, "You scored " .. score .. ". Button 1 plays again.") end overlayPrint(2, 4, "Runs kept: " .. played) overlayPrint(2, 6, "BEST RUNS") for place, row in ipairs(best) do overlayPrint(2, 6 + place, string.format("%d. %-8s %4d", place, row.stage, row.score)) end overlayPrint(24, 6, "BY STAGE") for line, row in ipairs(perStage) do overlayPrint(24, 6 + line, string.format("%-8s best %4d of %d", row.stage, row.top, row.tries)) end return OVERLAY_UPDATED end function onShutdown() addRun:finalize() runs:close() end refresh() ---- Lancez-le avec `Singe -R runs`. Le bouton 1 lance une manche et l'arrête, comme à la leçon vingt-huit, et le bouton 2, entre les manches, passe à l'épreuve suivante. Jouez-en quelques-unes sur chaque épreuve. Puis quittez et relancez : chaque partie est toujours là, les cinq meilleures à gauche et le meilleur score de chaque épreuve à droite. [#_what_just_happened_24] === Ce qui vient de se passer [source,lua] ---- local runs = sqlite3.open(singeGetDataPath() .. "runs.db") ---- Deux fichiers sont en jeu, et ils ne se ressemblent pas. `art/stages.db` est à vous : vous l'avez écrit, il est livré avec le jeu, et le jeu ne fait que le lire. `runs.db` est au jeu : il n'existait pas avant que le premier lancement le crée, il vit dans le dossier de données, et personne ne le publie. [source,lua] ---- if state == "waiting" then state = "result" score = 0 record(stages[stageAt].name, score) ---- Un bouton pressé trop tôt enregistre quand même une partie, avec un score de rien du tout. La base de données garde la trace de ce qui s'est passé, et un faux départ a bien eu lieu. Montrer ou non ces lignes, c'est une question pour le `SELECT`, pas quelque chose à cacher en ne les écrivant jamais. [source,lua] ---- for row in runs:nrows("SELECT stage, score FROM runs ORDER BY score DESC, id LIMIT 5") do ---- `ORDER BY score DESC, id` trie par score et, quand deux scores sont égaux, par `id` -- l'ordre dans lequel les parties ont été jouées. Sans la seconde clé, deux scores égaux reviennent dans l'ordre qui arrange SQLite, et cet ordre peut changer d'un lancement à l'autre. Un tableau qui se remélange sans raison visible a l'air cassé, même quand il ne l'est pas. [#_try_it_29] === Essayez . *Affichez la date.* `os.date("%Y-%m-%d", row.at)` retransforme une heure stockée en quelque chose qu'une personne peut lire. Ajoutez la date à chacune des meilleures parties. . *Ignorez les faux départs.* Ajoutez `WHERE score > 0` à la question des meilleures parties, entre `FROM runs` et `ORDER BY`. Puis comptez les faux départs sur chaque épreuve avec une question de votre cru. . *Ajoutez une épreuve.* Ouvrez `stages.db` dans un outil SQLite, ajoutez une ligne, et lancez le jeu. Rien ne change dans le script. C'est tout l'intérêt de garder les données hors du script. . *Remettez l'historique à zéro.* `runs:exec("DELETE FROM runs")` vide la table. Mettez-le sur un bouton, appelez `refresh` juste après, et regardez tout retomber à rien. C'est le « remettre à zéro les meilleurs scores » d'un menu de service. . *Écrivez mille lignes.* Appelez `record` mille fois dans une boucle et chronométrez avec `singeGetTicks`. Puis mettez `runs:exec("BEGIN")` avant la boucle et `runs:exec("COMMIT")` après, et chronométrez de nouveau. Une *transaction* fait de nombreuses écritures une seule, et la différence est assez grande pour mériter qu'on s'en souvienne. [#_break_it_on_purpose_28] === Cassez-le exprès Retirez `addRun:reset()` de `record` et jouez trois manches sur trois épreuves différentes. Aucune erreur. Le jeu tourne parfaitement. Mais le tableau de droite dit que vous avez joué la première épreuve trois fois, et les meilleures parties sont trois copies de votre premier score. `bind_values` n'a pas pu mettre de nouvelles valeurs dans une instruction déjà exécutée et pas remise à zéro, alors il a abandonné -- en silence, en renvoyant un nombre que vous ne vérifiiez pas -- et `step` a exécuté l'instruction de nouveau avec les valeurs qu'elle avait déjà. C'est le bogue qui compte dans cette leçon, parce qu'il n'en a pas l'air. Une requête qui échoue fait du bruit. Une écriture qui écrit la mauvaise chose est silencieuse, et vous la découvrez une semaine plus tard dans les données. Quand les lignes d'une base de données ne sont pas celles que vous attendez, regardez le code qui les y a mises avant le code qui les lit. Remettez `reset`, et cassez-le de la manière bruyante : changez `score` en `scroe` dans le premier `SELECT` de `refresh`. ---- Error running script: no such column: scroe stack traceback: [C]: in method 'nrows' runs/runs.singe:42: in local 'refresh' runs/runs.singe:132: in main chunk ---- SQLite lit les noms de colonnes quand il prépare la question, donc un nom mal orthographié arrête le jeu tout de suite, et le message le nomme exactement. La trace de pile se lit de l'intérieur vers l'extérieur : `nrows` s'est plaint, `refresh` l'a appelé à la ligne 42, et le script lui-même a appelé `refresh` à la ligne 132, la dernière, au démarrage du jeu. C'est pour ça que le message dit *running script* au lieu de nommer une fonction que le moteur a appelée : le jeu n'est jamais arrivé jusqu'à la première image. [#_what_you_learned_29] === Ce que vous avez appris * Une base de données est un fichier de tables ; une table, ce sont des lignes avec des colonnes qui ont un nom. * `require("sqlite3")` vous donne SQLite. `sqlite3.open` ouvre un fichier et le crée s'il manque. * Ce que le jeu écrit va dans `singeGetDataPath()`. Ce que vous publiez va dans votre dossier, ouvert avec `sqlite3.OPEN_READONLY`. * `CREATE TABLE IF NOT EXISTS` s'exécute sans risque à chaque démarrage du jeu. * Les valeurs entrent par `prepare`, `bind_values`, `step` et `reset`, jamais en collant des chaînes dans le SQL. * `nrows` rend les lignes sous forme de tables ; `urows` rend les colonnes elles-mêmes. * `ORDER BY`, `LIMIT`, `GROUP BY`, `MAX` et `COUNT` répondent aux questions pour lesquelles vous écririez sinon des boucles. * Demandez quand la réponse peut changer, pas à chaque image. * Dans `onShutdown`, on finalise les instructions, puis on ferme la base de données. [#_next_time_29] === La prochaine fois Votre jeu a maintenant un dossier plein de choses : un script, des images, des sons, et une base de données ou deux. La leçon trente transforme ce dossier en un seul fichier qu'un inconnu peut copier sur sa propre machine et jouer -- et votre base de données en lecture seule part avec, et s'ouvre toujours exactement comme maintenant.