[#_lesson_29_keeping_data] == Lección 29: Guardar datos image::learn/29-databases.png[La lección terminada, 480] La lección doce guardaba un récord: un número, bajo un nombre, ahí a la mañana siguiente. Eso es justo lo adecuado para un récord, y lo inadecuado para casi todo lo demás que un juego quiere conservar. Una lista de todas las partidas que haya jugado cualquiera. El mejor tiempo en cada una de veinte pistas. Cuarenta niveles que alguien diseñó en una hoja de cálculo. La pregunta «¿cuál es mi mejor marca en la fase difícil?», que un montón de números guardados no puede responder sin que escribas tú la respuesta a mano. Para eso está una base de datos, y Singe trae una incorporada. Esta lección toma la prueba de reflejos de la lección veintiocho, le da fases que vienen con el juego y hace que guarde cada partida que juegue cualquiera y responda preguntas sobre ellas. [#_what_a_database_is] === Qué es una base de datos Una base de datos es un archivo de *tablas*. Una tabla son filas y columnas, como una hoja de cálculo con los nombres de las columnas decididos de antemano: una tabla de partidas podría tener una fase, una puntuación y una hora en cada fila. Metes filas y haces preguntas para sacar filas, y las preguntas se escriben en *SQL*, un lenguaje pequeño que lleva haciendo este trabajo desde antes que la mayoría de los lenguajes de programación de los que has oído hablar. La base de datos que lleva Singe es *SQLite*. No es un servidor que haya que instalar ni un servicio en el que darse de alta. Es una biblioteca, la base de datos es un archivo normal y corriente, y el mismo SQLite está en tu móvil, en tu navegador y seguramente en tu coche. No hace falta saber SQL para seguir esta lección; cada sentencia se explica según va pasando, y las tres que más usarás -- `CREATE TABLE`, `INSERT` y `SELECT` -- están todas aquí. [#_opening_one] === Abrir una [source,lua] ---- local sqlite3 = require("sqlite3") local runs = sqlite3.open(singeGetDataPath() .. "runs.db") ---- `require("sqlite3")` carga una biblioteca que viene con Singe y te la devuelve como una tabla, así que todo lo que sabe hacer SQLite es `sqlite3.` algo. `sqlite3.open` abre un archivo de base de datos y lo crea si todavía no existe, así que la primera vez que se ejecuta tu juego crea uno vacío y todas las veces siguientes lo vuelve a encontrar. El archivo va en `singeGetDataPath()`, por la misma razón que la partida guardada de la lección doce: la carpeta del propio juego puede estar en un disco, en una carpeta compartida o empaquetada en un solo archivo, y en ninguno de esos sitios se puede escribir. Todo lo que escribe un juego va en su carpeta de datos. [#_making_a_table] === Crear una tabla Una base de datos vacía no tiene tablas. Creas una describiéndola: [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` ejecuta SQL que no devuelve nada. Los dobles corchetes son la cadena larga de Lua: todo lo que hay entre ellos, saltos de línea incluidos, es una sola cadena, y así es como mantienes el SQL legible. `CREATE TABLE` nombra la tabla y sus columnas, cada una con un tipo: `TEXT` para palabras e `INTEGER` para números enteros. `NOT NULL` significa que todas las filas tienen que tener un valor ahí. `INTEGER PRIMARY KEY` es especial: SQLite la rellena solo, contando hacia arriba, así que cada fila recibe su propio número sin que tengas que pensar en ello. `IF NOT EXISTS` es lo que hace que sea seguro ejecutar esto cada vez que arranca el juego. La primera vez, crea la tabla. Todas las veces siguientes, la tabla ya está ahí y la línea no hace nada. Sin esas tres palabras, la segunda vez que ejecutaras tu juego se pararía con un error diciendo que la tabla ya existe. [#_putting_a_row_in] === Meter una fila Podrías construir el SQL de una fila nueva pegando cadenas: [source,lua] ---- runs:exec("INSERT INTO runs (stage, score, at) VALUES ('" .. stage .. "', " .. score .. ", " .. os.time() .. ")") ---- No lo hagas. Funciona hasta que un nombre lleva un apóstrofo, y en ese momento la comilla termina la cadena antes de tiempo y la sentencia no tiene ni pies ni cabeza. En un juego que deja escribir a los jugadores es peor: lo que escriban pasa a formar parte de tu SQL. La forma correcta es escribir la sentencia una vez, con signos de interrogación donde van los valores, y pasar los valores aparte: [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` convierte el SQL en una *sentencia* que se puede usar una y otra vez. `bind_values` pone un valor en cada signo de interrogación, en orden. Contenga lo que contenga un valor, apóstrofos incluidos, es un valor y nunca parte del SQL. `step` ejecuta la sentencia, y `reset` la deja lista para ejecutarse otra vez. `os.time()` es Lua normal: la hora actual como un número de segundos, que es la forma más fácil de guardar «cuándo». Preparar una vez y reutilizar la sentencia también es más rápido, porque SQLite solo calcula una vez cómo ejecutarla. Para un juego que escribe una fila por minuto da igual. Para un editor de niveles que escribe mil, no. [#_asking_questions] === Hacer preguntas Las filas vuelven a salir con `nrows`, que ejecuta un `SELECT` y te da cada fila como una tabla con los nombres de las columnas como claves: [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 ---- Lee el SQL de izquierda a derecha. `SELECT stage, score` dice qué columnas quieres. `FROM runs` dice de qué tabla. `ORDER BY score DESC` ordena las filas por puntuación, de mayor a menor, y `LIMIT 5` se para después de cinco. Eso es una tabla de récords, y no ha necesitado ni una línea tuya de código para ordenar. Las preguntas pueden ser más listas que las que querrías escribir a mano: [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` reúne las filas en un grupo por fase, y `MAX(score)` y `COUNT(*)` se calculan para cada grupo: la mejor puntuación de esa fase y cuántas veces se ha jugado. `AS top` le pone nombre a la respuesta, que es la clave con la que la vuelves a leer. Cuando una pregunta tiene una sola respuesta, `urows` te devuelve las columnas mismas en lugar de una tabla, que queda más limpio para un recuento: [source,lua] ---- for count in runs:urows("SELECT COUNT(*) FROM runs") do played = count end ---- [#_ask_when_it_changes] === Pregunta cuando cambie Ninguna de esas preguntas va en `onOverlayUpdate`. Una consulta es rápida, pero sesenta por segundo, para obtener respuestas que cambian una vez por minuto, es el juego haciendo la misma pregunta y recibiendo la misma respuesta cincuenta y nueve veces de cada sesenta. Así que el juego pregunta cuando la respuesta puede haber cambiado -- una vez al principio, y otra cada vez que se registra una partida -- y guarda lo que le llega en tablas normales: [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 -- y lo mismo para las otras dos preguntas end ---- `onOverlayUpdate` dibuja a partir de `best`, que no cuesta nada. `record` llama a `refresh` después de cada fila nueva, así que la tabla de la pantalla nunca está desfasada. [#_data_that_ships_with_the_game] === Datos que vienen con el juego La otra mitad de los datos de un juego es la parte que escribes antes de que juegue nadie: las fases, los niveles, los objetos, las líneas de diálogo. Eso también puede ser una base de datos, hecha con cualquier herramienta de SQLite -- el programa `sqlite3` en la línea de comandos, o un programa con ventanas como DB Browser for SQLite -- y puesta en la carpeta de tu juego junto a su arte. El kit de arte trae una, `stages.db`, con un nombre y la espera más corta y la más larga de cada fase. Esa la abre un juego de otra manera: [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` es la carpeta de tu juego, donde está el arte. `sqlite3.OPEN_READONLY` dice que solo piensas leerla, y eso importa el día que publiques. Cuando empaquetes tu juego en un solo archivo en la próxima lección, una base de datos abierta de solo lectura se lee directamente de ese archivo, sin desempaquetar nada. Una abierta para escritura se copia, una vez, a la carpeta de datos del juego, y a partir de entonces lo que cambias es la copia; en el archivo que publicaste no se escribe nunca. Así que mantén separados los dos tipos. Lo que has hecho tú va en tu carpeta y se abre de solo lectura. Lo que hace el juego va en la carpeta de datos. Aquí las fases se leen una vez al principio en una tabla normal y la base de datos se cierra, porque una lista de cuatro fases no merece tener un archivo abierto. [#_closing_up] === Cerrar al terminar Las sentencias y las bases de datos retienen memoria y mantienen un archivo abierto hasta que dices que has terminado, y el sitio ordenado para decirlo es `onShutdown`: [source,lua] ---- function onShutdown() addRun:finalize() runs:close() end ---- `finalize` es el `close` de una sentencia. Una base de datos no se cierra mientras siga por ahí una sentencia hecha a partir de ella, así que las sentencias van primero. [#_the_whole_script_4] === El script completo Crea una carpeta llamada `runs`, copia dentro la carpeta `art` del libro para tener `stages.db`, y pon esto en `runs.singe`: [source,lua] ---- dofile("Singe/Framework.singe") local sqlite3 = require("sqlite3") local PERFECT = 1000 -- Lo que viene con el juego: las fases, leídas una vez y cerradas. 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() -- Lo que escribe el juego: cada partida que juegue cualquiera, guardada en la carpeta de datos. 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 -- Las preguntas, hechas cuando se añade una partida y no en cada fotograma. 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() ---- Ejecútalo con `Singe -R runs`. El botón 1 empieza una ronda y la detiene, como en la lección veintiocho, y el botón 2 entre rondas pasa a la fase siguiente. Juega unas cuantas en cada una. Luego sal y vuelve a ejecutarlo: todas las partidas siguen ahí, las cinco mejores a la izquierda y la mejor de cada fase a la derecha. [#_what_just_happened_24] === Qué acaba de pasar [source,lua] ---- local runs = sqlite3.open(singeGetDataPath() .. "runs.db") ---- Hay dos archivos en juego, y no se parecen. `art/stages.db` es tuyo: lo escribiste tú, viene con el juego y el juego solo lo lee. `runs.db` es del juego: no existía hasta que la primera ejecución lo creó, vive en la carpeta de datos y nadie lo publica. [source,lua] ---- if state == "waiting" then state = "result" score = 0 record(stages[stageAt].name, score) ---- Un botón pulsado demasiado pronto también registra una partida, con una puntuación de nada. La base de datos es un registro de lo que pasó, y una salida en falso pasó. Si mostrar esas filas o no es cosa del `SELECT`, no algo que esconder no escribiéndolas nunca. [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` ordena por puntuación y, cuando dos puntuaciones son iguales, por `id` -- el orden en que se jugaron. Sin el segundo criterio, dos puntuaciones iguales salen en el orden que a SQLite le venga bien, que puede cambiar de una ejecución a otra. Una tabla que se reordena sola sin motivo visible parece rota aunque no lo esté. [#_try_it_29] === Pruébalo . *Muestra la fecha.* `os.date("%Y-%m-%d", row.at)` vuelve a convertir una hora guardada en algo que una persona puede leer. Añade la fecha a cada una de las mejores partidas. . *Ignora las salidas en falso.* Añade `WHERE score > 0` a la pregunta de las mejores partidas, entre `FROM runs` y `ORDER BY`. Luego cuenta las salidas en falso de cada fase con una pregunta tuya. . *Añade una fase.* Abre `stages.db` en una herramienta de SQLite, añade una fila y ejecuta el juego. No cambia nada del script. Esa es la gracia de tener los datos fuera del script. . *Borra el historial.* `runs:exec("DELETE FROM runs")` vacía la tabla. Ponlo en un botón, llama a `refresh` después y mira cómo todo se queda en nada. Este es el «borrar récords» de un menú de servicio. . *Escribe mil filas.* Llama a `record` mil veces en un bucle y cronométralo con `singeGetTicks`. Luego pon `runs:exec("BEGIN")` antes del bucle y `runs:exec("COMMIT")` después, y vuelve a cronometrarlo. Una *transacción* convierte muchas escrituras en una, y la diferencia es lo bastante grande como para que valga la pena recordarla. [#_break_it_on_purpose_28] === Rómpelo a propósito Quita `addRun:reset()` de `record` y juega tres rondas en tres fases distintas. Ningún error. El juego funciona perfectamente. Pero la tabla de la derecha dice que has jugado la primera fase tres veces, y las mejores partidas son tres copias de tu primera puntuación. `bind_values` no podía poner valores nuevos en una sentencia que ya se había ejecutado y no se había reiniciado, así que se rindió -- en silencio, devolviendo un número que no estabas comprobando -- y `step` volvió a ejecutar la sentencia con los valores que ya tenía. Este es el fallo que importa en esta lección, porque no lo parece. Una consulta que falla hace ruido. Una escritura que escribe lo que no es no hace ninguno, y la encuentras una semana después en los datos. Cuando las filas de una base de datos no son lo que esperas, mira el código que las puso ahí antes que el código que las lee. Vuelve a poner `reset` y rómpelo de la forma ruidosa: cambia `score` por `scroe` en el primer `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 lee los nombres de las columnas cuando prepara la pregunta, así que uno mal escrito para el juego en el acto, y el mensaje lo nombra exactamente. La traza se lee de dentro hacia fuera: `nrows` se quejó, `refresh` la llamó en la línea 42, y el propio script llamó a `refresh` en la línea 132, la última, al arrancar el juego. Por eso dice *running script* en lugar de nombrar una función que llamó el motor: nunca llegó al primer fotograma. [#_what_you_learned_29] === Lo que has aprendido * Una base de datos es un archivo de tablas; una tabla son filas con columnas que tienen nombre. * `require("sqlite3")` te da SQLite. `sqlite3.open` abre un archivo y lo crea si no existe. * Lo que escribe el juego va en `singeGetDataPath()`. Lo que publicas va en tu carpeta, y se abre con `sqlite3.OPEN_READONLY`. * `CREATE TABLE IF NOT EXISTS` se puede ejecutar sin peligro cada vez que arranca el juego. * Los valores entran con `prepare`, `bind_values`, `step` y `reset`, nunca pegando cadenas en el SQL. * `nrows` devuelve las filas como tablas; `urows` devuelve las columnas mismas. * `ORDER BY`, `LIMIT`, `GROUP BY`, `MAX` y `COUNT` responden a las preguntas para las que, si no, escribirías bucles. * Pregunta cuando la respuesta puede cambiar, no en cada fotograma. * En `onShutdown` se finalizan las sentencias y después se cierra la base de datos. [#_next_time_29] === La próxima vez Tu juego tiene ya una carpeta llena de cosas: un script, arte, sonidos y una base de datos o dos. La lección treinta convierte esa carpeta en un solo archivo que un desconocido puede copiar en su propio ordenador y jugar -- y tu base de datos de solo lectura va con él, abriéndose exactamente igual que ahora.