[#_lesson_7_a_game] == Lección 7: Un juego image::learn/07-a-game.png[La lección terminada, 480] Hace seis lecciones pusiste una palabra en una pantalla negra. Desde entonces has aprendido todas las ideas de las que está hecho un juego pequeño: variables, decisiones, bucles, funciones propias y listas de cosas. Esta lección no enseña ninguna idea nueva. Gasta las que ya tienes. Vas a construir un juego llamado Dodge. Caen bloques del cielo, tú deslizas una barra por la parte de abajo para apartarte de su camino, y cada bloque que no te da es un punto. Si te dan tres, se acabó. Lo construirás en seis etapas, y todas funcionan: nunca tendrás que escribir cien líneas y cruzar los dedos. [#_make_the_folder] === Crea la carpeta Crea una carpeta llamada `dodge`, y dentro un archivo llamado `dodge.singe`. Ejecútalo como llevas ejecutándolo todo desde la lección uno: ---- Singe -R dodge ---- Déjalo funcionando en una terminal durante toda la lección. Cada vez que guardes, el juego se reinicia con tu cambio dentro. [#_stage_one_something_you_can_steer] === Primera etapa: algo que puedes manejar Escribe esto, guárdalo y manéjalo con las teclas de flecha izquierda y derecha. [source,lua] ---- dofile("Singe/Framework.singe") local PLAYER_WIDTH = 40 local PLAYER_HEIGHT = 8 local PLAYER_SPEED = 4 local PLAYER_MARGIN = 4 local screenWidth = overlayGetWidth() local screenHeight = overlayGetHeight() local playerY = screenHeight - PLAYER_HEIGHT - PLAYER_MARGIN local playerX = (screenWidth - PLAYER_WIDTH) / 2 local goingLeft = false local goingRight = false function onInputPressed(what) if what == SWITCH_LEFT then goingLeft = true elseif what == SWITCH_RIGHT then goingRight = true end end function onInputReleased(what) if what == SWITCH_LEFT then goingLeft = false elseif what == SWITCH_RIGHT then goingRight = false end end function onOverlayUpdate() if goingLeft then playerX = playerX - PLAYER_SPEED end if goingRight then playerX = playerX + PLAYER_SPEED end if playerX < 0 then playerX = 0 elseif playerX > screenWidth - PLAYER_WIDTH then playerX = screenWidth - PLAYER_WIDTH end overlayClear() colorForeground(80, 255, 160) overlayBox(playerX, playerY, playerX + PLAYER_WIDTH - 1, playerY + PLAYER_HEIGHT - 1) return OVERLAY_UPDATED end ---- Cincuenta líneas, y casi nada es nuevo. Hay dos cosas que sí lo son, así que empieza por la primera línea. [source,lua] ---- dofile("Singe/Framework.singe") ---- `dofile` es de Lua y no de Singe, y significa «ve y ejecuta ese archivo, y luego sigue aquí». El archivo que ejecuta es `Singe/Framework.singe`, uno de los archivos que el motor desempaquetó en tu carpeta de trabajo la primera vez que ejecutaste algo, allá en la lección uno. Ábrelo en tu editor de texto y míralo: es Lua normal, del mismo tipo que lo que llevas escribiendo desde la lección dos, y no tiene nada de mágico. Ejecutarlo le da a tu script un conjunto de nombres que luego puede usar. Los que usa este libro son estos: * `DIR`, que guarda la carpeta en la que está tu propio script. La lección nueve lo necesita en cuanto un juego carga una imagen, y es la verdadera razón de que esta línea esté aquí. * `SCANCODE` y `MODIFIER`, nombres para cada tecla de un teclado, para un juego que lee lo que se escribe en lugar de un joystick. * De `GAMEPAD_0` a `GAMEPAD_3` y de `MOUSE_0` a `MOUSE_3`, para una recreativa que tiene enchufado más de uno de algo. * Unas cuantas funciones de ayuda cuyos nombres empiezan por `util`, y las seis llamadas que envían una puntuación a una tabla de récords, que es la lección veintiocho. * Otra forma completamente distinta de escribir un juego, que es la lección quince. Hay una cosa que *no* te da, y la menciono porque es lo natural suponerlo. `SWITCH_LEFT`, y cualquier otro nombre que empiece por `SWITCH_`, viene del propio motor, antes de que se ejecute ningún script. Por eso las lecciones de la tres a la seis podían leer las teclas de flecha sin cargar nada. El framework añade cosas a lo que ya está ahí; no es él quien lo pone. Este juego en concreto no usa nada del framework. La línea está aquí porque todos los juegos desde la lección nueve hasta el final del libro sí lo necesitan, y porque es como empiezan casi todos los juegos de Singe de verdad. Se lee una vez, al arrancar, y luego te olvidas de él. La segunda cosa nueva es la forma de tratar las teclas de flecha, porque es lo primero que los principiantes hacen mal. `onInputPressed` no mueve al jugador. Pone `goingLeft` a `true`, y `onInputReleased` lo vuelve a poner a `false`. El movimiento ocurre en `onOverlayUpdate`, que pregunta «¿está el jugador pulsando la izquierda?» sesenta veces por segundo. Si movieras al jugador dentro de `onInputPressed`, tendrías un paso por pulsación: Singe envía una pulsación exactamente una vez, cuando la tecla baja, y nunca la repite, cosa que la entrada de `onInputPressed` en la referencia dice claramente. Mantener pulsada la tecla no haría nada en absoluto. Así que el callback de pulsación apunta lo que es verdad en el mundo, y la actualización decide qué hacer con ello. Mantén esa separación y tu código de entrada tendrá cinco líneas para siempre. Los cuatro nombres en mayúsculas son variables normales. Escribir `PLAYER_SPEED` en mayúsculas es un mensaje para ti y para quien lo lea: esta se fija una vez arriba y no cambia nunca. Nada en Lua lo obliga. Importa porque dentro de un momento habrá diez, y cuando el juego sea demasiado difícil querrás un sitio obvio al que ir a bajar un número. `screenWidth` y `screenHeight` vienen del motor en lugar de escribirse a mano, así que el juego se adapta al tamaño que tenga el overlay. `playerY` se calcula una vez a partir de la altura: la barra está a `PLAYER_MARGIN` píxeles del borde de abajo y nunca sube ni baja, así que no hay razón para volver a calcularlo en cada fotograma. El `- 1` de la línea de `overlayBox` no es una errata. `overlayBox` dibuja incluyendo las dos esquinas que le das, así que una caja de `playerX` a `playerX + 40` tiene cuarenta y un píxeles de ancho. Restar uno hace que la barra que ves tenga exactamente cuarenta de ancho, lo que importará en la cuarta etapa, cuando algo tenga que decidir si te ha tocado. [#_stage_two_something_to_dodge] === Segunda etapa: algo que esquivar Un bloque, cayendo. Añade dos constantes a la lista de arriba: [source,lua] ---- local BLOCK_SIZE = 12 local BLOCK_SPEED = 1.5 ---- Dos variables más debajo de `goingRight`: [source,lua] ---- local blockX = math.random(0, screenWidth - BLOCK_SIZE) local blockY = -BLOCK_SIZE ---- En `onOverlayUpdate`, justo encima de la línea `overlayClear()`, haz que caiga: [source,lua] ---- blockY = blockY + BLOCK_SPEED if blockY > screenHeight then blockX = math.random(0, screenWidth - BLOCK_SIZE) blockY = -BLOCK_SIZE end ---- Y debajo de la línea que dibuja al jugador, dibújalo: [source,lua] ---- colorForeground(255, 90, 90) overlayBox(blockX, blockY, blockX + BLOCK_SIZE - 1, blockY + BLOCK_SIZE - 1) ---- Guarda. Un cuadrado rojo cae, llega abajo y vuelve a aparecer arriba en otro sitio. Empieza en `-BLOCK_SIZE`, que está por encima del borde superior de la pantalla. Una posición negativa no tiene nada de malo: el motor dibuja la parte de la caja que está en el overlay y tira sin decir nada la parte que no. El bloque entra deslizándose en lugar de aparecer de golpe. `BLOCK_SPEED` es `1.5`, no un número entero, así que `blockY` pasa la mayor parte de su vida valiendo algo como `83.5`. Eso también está bien. Al dibujar se corta la fracción, y conservarla en la variable es lo que permite que el bloque se mueva más despacio que un píxel por fotograma. [#_stage_three_a_sky_full_of_them] === Tercera etapa: un cielo lleno de ellos Un bloque no es un juego. En la lección seis aprendiste a guardar muchos de algo en una lista, y esto es para lo que servía. Borra `blockX` y `blockY`, y pon esto en su lugar: [source,lua] ---- local blocks = {} local spawnTimer = 0 ---- Añade una constante más arriba: [source,lua] ---- local SPAWN_FRAMES = 40 ---- Ahora una función que crea un bloque y lo mete en la lista. Ponla encima de `onInputPressed`: [source,lua] ---- function spawnBlock() local block = {} block.x = math.random(0, screenWidth - BLOCK_SIZE) block.y = -BLOCK_SIZE table.insert(blocks, block) end ---- Sustituye el código de caída de la segunda etapa por esto: [source,lua] ---- spawnTimer = spawnTimer - 1 if spawnTimer <= 0 then spawnBlock() spawnTimer = SPAWN_FRAMES end for i = #blocks, 1, -1 do local block = blocks[i] block.y = block.y + BLOCK_SPEED if block.y > screenHeight then table.remove(blocks, i) end end ---- Y sustituye la única línea que dibujaba el bloque por un bucle que los recorre todos: [source,lua] ---- for i, block in ipairs(blocks) do overlayBox(block.x, block.y, block.x + BLOCK_SIZE - 1, block.y + BLOCK_SIZE - 1) end ---- Guarda. Llueven bloques. Un bloque es un registro: una tablita con una `x` y una `y` dentro, que es como evitas que dos números que van juntos se separen. `blocks` es una lista de esos registros, que es la forma que tiene el mundo de casi cualquier juego. `spawnTimer` cuenta hacia atrás de uno en uno en cada fotograma, y cuando llega a cero nace un bloque y el temporizador se vuelve a cargar. Cuarenta fotogramas son unos dos tercios de segundo. En este juego no hay reloj y no hace falta: `onOverlayUpdate` es el latido, y contar latidos es una forma perfectamente buena de medir el tiempo. El bucle de caída va *hacia atrás* -- de `#blocks` a `1`, de uno en uno -- porque va quitando cosas. Es la regla de la lección seis, y esta es la primera vez que tienes una razón de verdad para cumplirla. Quita el elemento tres recorriendo hacia delante y todo se corre un puesto hacia abajo, así que el bloque que era el elemento cuatro ahora es el tres y el bucle no lo mira nunca. Yendo hacia atrás, todo lo que se mueve ya se ha tratado. El bucle de dibujo usa `ipairs`, porque no quita nada y solo quiere cada bloque por turno. [#_stage_four_getting_hit] === Cuarta etapa: recibir un golpe Los bloques te atraviesan. Es hora de darse cuenta. El motor tiene una función justo para esta pregunta. Cambia la comprobación de dentro del bucle de caída de esto: [source,lua] ---- if block.y > screenHeight then table.remove(blocks, i) end ---- a esto: [source,lua] ---- if collideRects(block.x, block.y, BLOCK_SIZE, BLOCK_SIZE, playerX, playerY, PLAYER_WIDTH, PLAYER_HEIGHT) or block.y > screenHeight then table.remove(blocks, i) end ---- Guarda, y lleva la barra contra un bloque que cae. Desaparece. `collideRects` recibe dos rectángulos y responde `true` si se solapan. Cada rectángulo son cuatro números: una esquina, y luego un ancho y un alto -- *no* dos esquinas, que es la otra forma razonable de describir un rectángulo y la que usa `overlayBox`. La entrada de `collideRects` en la referencia da el orden, y vale la pena mirarla ahora, porque poner los ocho argumentos en el orden equivocado es el tipo de equivocación que no produce ningún error, solo un juego en el que nada choca nunca con nada. Podrías escribirla tú. Son cuatro comparaciones, y de verdad que no es difícil. Usa la del motor de todos modos, por tres razones. Es correcta, también en los casos difíciles: un rectángulo sin ancho, dos rectángulos que solo se tocan por un borde. Dice lo que significa, así que la línea se lee como «¿se han solapado estos dos?» en lugar de cuatro comparaciones unidas por `and` que tienes que descifrar cada vez que vuelves a ella. Y cuando llegues a la lección once, donde las cosas chocan constantemente, ya conocerás la familia: `collidePointRect` para un clic del ratón, `collideCircles` para dos cosas redondas y `collideSegments` para un disparo contra una pared. La referencia las enumera juntas. Lo único que el motor no puede hacer por ti es decidir qué *significa* un rectángulo. Aquí el rectángulo del bloque es el bloque entero. En un juego de verdad a menudo quieres que el rectángulo de impacto sea un poco más pequeño que la imagen, para que librarte por poco se sienta como librarte por poco. Eso es una decisión de diseño, y la lección once es donde la tomas. [#_stage_five_score_and_lives] === Quinta etapa: puntuación y vidas Que te den debería costar algo, y sobrevivir debería valer algo. Dos constantes más arriba: [source,lua] ---- local BLOCK_FASTER = 0.05 local START_LIVES = 3 ---- Tres variables más, junto a `spawnTimer`: [source,lua] ---- local blockSpeed = BLOCK_SPEED local score = 0 local lives = START_LIVES ---- `BLOCK_SPEED` es ahora la velocidad a la que *empieza* el juego, y `blockSpeed` es la velocidad a la que va ahora mismo. Cambia la línea de caída para que use la variable: [source,lua] ---- block.y = block.y + blockSpeed ---- Luego separa los dos casos, porque ahora hacen cosas distintas: [source,lua] ---- if collideRects(block.x, block.y, BLOCK_SIZE, BLOCK_SIZE, playerX, playerY, PLAYER_WIDTH, PLAYER_HEIGHT) then table.remove(blocks, i) lives = lives - 1 elseif block.y > screenHeight then table.remove(blocks, i) score = score + 1 blockSpeed = blockSpeed + BLOCK_FASTER end ---- Y muestra los números. Pon esto justo antes del `return`: [source,lua] ---- overlayPrint(1, 1, "SCORE " .. score .. " LIVES " .. lives) ---- Guarda, y juega un minuto. Se va poniendo más difícil, y ese es todo el truco: cada bloque que esquivas hace que los siguientes caigan una vigésima de píxel por fotograma más deprisa. A los veinte puntos ya van claramente más rápido. No has tenido que escribir ni una palabra sobre niveles de dificultad. También verás `LIVES` llegar a `-4`, porque todavía nada detiene el juego. Esa es la siguiente etapa, y es la que más importa. [#_stage_six_game_over_and_going_again] === Sexta etapa: fin de partida y volver a jugar Aquí está la trampa. La forma obvia de añadir una pantalla de fin de partida es una variable llamada `gameOver`, que se pone a `true` cuando se acaban las vidas. Luego quieres una pantalla de título antes de la primera partida, así que añades `started`. Luego quieres una tecla de pausa, así que añades `paused`. Ahora tienes tres variables de verdadero o falso, ocho combinaciones entre ellas, y cinco de esas combinaciones son disparates que tu código tiene que tener cuidado de no producir nunca. Todos los principiantes escriben este juego al menos una vez, y es donde los juegos pequeños van a morir. Haz esto en su lugar. Una sola variable guarda *lo que está haciendo el juego*, como una palabra: [source,lua] ---- local state = "waiting" ---- Vale `"waiting"` antes de la primera partida, `"playing"` durante una y `"over"` cuando se acaban las vidas. Tres estados, una variable y ninguna combinación imposible. Las cadenas no tienen nada de mágico; son palabras que has elegido tú, y tienes que escribirlas igual todas las veces. Ahora la función que empieza una partida. Cada variable que pertenece a una partida vuelve aquí a su valor inicial, en un solo sitio: [source,lua] ---- function startGame() playerX = (screenWidth - PLAYER_WIDTH) / 2 blocks = {} blockSpeed = BLOCK_SPEED spawnTimer = 0 score = 0 lives = START_LIVES state = "playing" end ---- Perder la última vida la termina. Dentro de la rama de la colisión, debajo de `lives = lives - 1`: [source,lua] ---- if lives == 0 then state = "over" end ---- La barra espaciadora empieza una partida, pero solo cuando no hay una ya en marcha. Añade una rama a `onInputPressed`: [source,lua] ---- elseif what == SWITCH_BUTTON1 and state ~= "playing" then startGame() ---- `~=` es «no es igual a». `state ~= "playing"` es verdadero mientras se espera y verdadero cuando se ha acabado, que son justo los dos momentos en los que la barra espaciadora debe empezar una partida. `SWITCH_BUTTON1` es la barra espaciadora por defecto, y también el botón A de un mando. Dos funciones pequeñas ponen las palabras en pantalla. `printCentered` hace las cuentas una sola vez para que las pantallas de título y de fin de partida no las hagan mal cada una por su lado: [source,lua] ---- function drawOverText() printCentered(7, "GAME OVER") printCentered(9, "PRESS SPACE TO PLAY AGAIN") end function drawWaitingText() printCentered(6, "DODGE THE BLOCKS") printCentered(8, "ARROW KEYS TO MOVE") printCentered(10, "PRESS SPACE TO START") end function printCentered(row, text) overlayPrint(math.floor((TEXT_COLUMNS - #text) / 2), row, text) end ---- con una constante más arriba: [source,lua] ---- local TEXT_COLUMNS = overlayGetWidth() // overlayGetFontWidth() ---- Por último, la parte importante. Saca de `onOverlayUpdate` todo lo que dibuja y llévalo a una función llamada `drawGame`, lleva todo lo que mueve a una función llamada `updatePlaying` -- las dos están en el listado completo de abajo -- y deja `onOverlayUpdate` así: [source,lua] ---- function onOverlayUpdate() if state == "playing" then updatePlaying() end overlayClear() drawGame() if state == "waiting" then drawWaitingText() elseif state == "over" then drawOverText() end return OVERLAY_UPDATED end ---- Guarda. Tienes un juego: una pantalla de título, una partida, un fin de partida y una forma de volver a empezar. Vuelve a leer esas trece líneas, porque son la forma de todos los juegos que vas a escribir. El mundo solo se mueve mientras el estado es `"playing"`, así que la pantalla de fin de partida se congela con los últimos bloques colgando justo donde estaban. El mundo se dibuja en cada fotograma sea cual sea el estado, porque la imagen es la misma en los tres; el estado solo decide qué palabras van encima. Y hay exactamente un sitio que toma esa decisión. Eso es lo que has comprado con la variable `state`. Cuando añadas una pausa, añadirás una rama a ese `if` y una línea a `onInputPressed`, y nada más del juego tendrá que saber nada de ella. [#_the_whole_thing_6] === Todo junto Aquí lo tienes, terminado. Si el tuyo no coincide, vale la pena encontrar la diferencia. [source,lua] ---- dofile("Singe/Framework.singe") local PLAYER_WIDTH = 40 local PLAYER_HEIGHT = 8 local PLAYER_SPEED = 4 local PLAYER_MARGIN = 4 local BLOCK_SIZE = 12 local BLOCK_SPEED = 1.5 local BLOCK_FASTER = 0.05 local SPAWN_FRAMES = 40 local START_LIVES = 3 local TEXT_COLUMNS = overlayGetWidth() // overlayGetFontWidth() local screenWidth = overlayGetWidth() local screenHeight = overlayGetHeight() local playerY = screenHeight - PLAYER_HEIGHT - PLAYER_MARGIN local playerX = (screenWidth - PLAYER_WIDTH) / 2 local goingLeft = false local goingRight = false local blocks = {} local blockSpeed = BLOCK_SPEED local spawnTimer = 0 local score = 0 local lives = START_LIVES local state = "waiting" function drawGame() colorForeground(80, 255, 160) overlayBox(playerX, playerY, playerX + PLAYER_WIDTH - 1, playerY + PLAYER_HEIGHT - 1) colorForeground(255, 90, 90) for i, block in ipairs(blocks) do overlayBox(block.x, block.y, block.x + BLOCK_SIZE - 1, block.y + BLOCK_SIZE - 1) end overlayPrint(1, 1, "SCORE " .. score .. " LIVES " .. lives) end function drawOverText() printCentered(7, "GAME OVER") printCentered(9, "PRESS SPACE TO PLAY AGAIN") end function drawWaitingText() printCentered(6, "DODGE THE BLOCKS") printCentered(8, "ARROW KEYS TO MOVE") printCentered(10, "PRESS SPACE TO START") end function printCentered(row, text) overlayPrint(math.floor((TEXT_COLUMNS - #text) / 2), row, text) end function spawnBlock() local block = {} block.x = math.random(0, screenWidth - BLOCK_SIZE) block.y = -BLOCK_SIZE table.insert(blocks, block) end function startGame() playerX = (screenWidth - PLAYER_WIDTH) / 2 blocks = {} blockSpeed = BLOCK_SPEED spawnTimer = 0 score = 0 lives = START_LIVES state = "playing" end function updatePlaying() if goingLeft then playerX = playerX - PLAYER_SPEED end if goingRight then playerX = playerX + PLAYER_SPEED end if playerX < 0 then playerX = 0 elseif playerX > screenWidth - PLAYER_WIDTH then playerX = screenWidth - PLAYER_WIDTH end spawnTimer = spawnTimer - 1 if spawnTimer <= 0 then spawnBlock() spawnTimer = SPAWN_FRAMES end for i = #blocks, 1, -1 do local block = blocks[i] block.y = block.y + blockSpeed if collideRects(block.x, block.y, BLOCK_SIZE, BLOCK_SIZE, playerX, playerY, PLAYER_WIDTH, PLAYER_HEIGHT) then table.remove(blocks, i) lives = lives - 1 if lives == 0 then state = "over" end elseif block.y > screenHeight then table.remove(blocks, i) score = score + 1 blockSpeed = blockSpeed + BLOCK_FASTER end end end function onInputPressed(what) if what == SWITCH_LEFT then goingLeft = true elseif what == SWITCH_RIGHT then goingRight = true elseif what == SWITCH_BUTTON1 and state ~= "playing" then startGame() end end function onInputReleased(what) if what == SWITCH_LEFT then goingLeft = false elseif what == SWITCH_RIGHT then goingRight = false end end function onOverlayUpdate() if state == "playing" then updatePlaying() end overlayClear() drawGame() if state == "waiting" then drawWaitingText() elseif state == "over" then drawOverText() end return OVERLAY_UPDATED end ---- Unas ciento veinte líneas de código, y las has escrito todas de unas pocas en unas pocas. [#_what_just_happened_4] === Qué acaba de pasar Algunas partes merecen una segunda mirada. [source,lua] ---- local blocks = {} local score = 0 function startGame() score = 0 end ---- Los datos son `local` y las funciones no. Son dos decisiones deliberadas y opuestas. Los datos son `local` para que pertenezcan a este archivo y nada de fuera pueda meter la mano y cambiarlos. Las funciones son globales para que puedan llamarse unas a otras en cualquier orden: `drawOverText` se define antes que `printCentered` y la llama de todos modos, porque un nombre global se busca en el momento de la llamada, y para entonces ya se ha leído todo el archivo. Haz `local` esas funciones y el archivo tendrá que estar ordenado para que nada se use antes de aparecer, que es una regla con la que tropezarías durante el resto del libro. Tres de los nombres de aquí no tienen elección: `onOverlayUpdate` y los dos callbacks de entrada tienen que ser globales, porque así es como los encuentra Singe. Dentro del archivo, el orden es alfabético, con los tres callbacks al final. No es una regla de Lua. Es para que dentro de seis meses puedas encontrar `spawnBlock` sin leer nada. [source,lua] ---- overlayPrint(math.floor((TEXT_COLUMNS - #text) / 2), row, text) ---- `+#text+` es la longitud de una cadena, en caracteres. Conociste `+#+` en la lección seis contando los elementos de una lista, y aquí hace el mismo trabajo. `"GAME OVER"` tiene nueve caracteres, así que en una pantalla de sesenta columnas empieza en la columna veinticinco, lo que deja veinticinco celdas de espacio a su izquierda y veintiséis a su derecha. `math.floor` tira la fracción, porque no existe la columna veinticinco y media. `TEXT_COLUMNS` calcula cuántas celdas de carácter caben a lo ancho de la pantalla. `overlayPrint` cuenta en celdas mientras que todo lo demás cuenta en píxeles, así que el ancho del overlay se divide entre el ancho de una celda, y `overlayGetFontWidth` es lo que te lo dice. Dos barras en lugar de una, porque una fracción de columna no es un sitio donde imprimir. Calcúlalo así y el centrado sigue siendo correcto con cualquier tamaño de overlay, que es lo cuarto de *Pruébalo*. [source,lua] ---- colorForeground(255, 90, 90) ---- `colorForeground` fija un color que se queda fijado. No es un argumento de `overlayBox`; es un interruptor que accionas, y todo lo que se dibuja después sale en ese color hasta que lo vuelves a accionar. Por eso `drawGame` pone el verde, dibuja al jugador, pone el rojo y luego dibuja todos los bloques sin volver a mencionar el color. No afecta a `overlayPrint`. La fuente de consola se copia en el overlay tal cual, en sus propios colores, cosa que la entrada de `overlayPrint` en la referencia dice con todas las letras. El texto en color necesita una fuente cargada, y eso es la lección diecinueve. [#_try_it_7] === Pruébalo . *Hazlo más fácil, y luego injusto.* Cambia `PLAYER_WIDTH` a `80` y juega una partida. Luego cámbialo a `12`, el mismo tamaño que un bloque. Fíjate en que no has tenido que tocar ni el dibujo ni la colisión para ninguna de las dos cosas. . *Que acelere más deprisa.* Cambia `BLOCK_FASTER` de `0.05` a `0.5` y mira cuántos puntos consigues. Luego prueba `0`, y pregúntate si el juego sigue mereciendo la pena. . *Una vida extra.* Devuélvele una vida al jugador cada diez puntos. Todo lo que necesitas ya está en `updatePlaying`, en la rama que suma a la puntuación. . *Juega en grande.* Ejecútalo con `Singe -R --canvas=1280x720 dodge`. El juego se coloca bien, texto centrado incluido, porque le preguntó al motor cuánto medía la pantalla. `TEXT_COLUMNS` también lo preguntó. . *Ponlo en pausa.* Añade un cuarto estado, `"paused"`. `SWITCH_BUTTON2` debería cambiar entre `"playing"` y `"paused"`, y nada debería moverse en pausa. Bien hecho, son tres líneas y no tienes que tocar `updatePlaying` en absoluto. [#_break_it_on_purpose_7] === Rómpelo a propósito Ve a `spawnBlock` y borra la línea que da valor a `block.y`, para que quede así: [source,lua] ---- function spawnBlock() local block = {} block.x = math.random(0, screenWidth - BLOCK_SIZE) table.insert(blocks, block) end ---- Guarda, pulsa la barra espaciadora, y Singe se detiene con esto: ---- Error executing function 'onOverlayUpdate': dodge/dodge.singe:97: attempt to perform arithmetic on a nil value (field 'y') stack traceback: dodge/dodge.singe:97: in function 'updatePlaying' dodge/dodge.singe:135: in function 'onOverlayUpdate' ---- La línea 97 es `block.y = block.y + blockSpeed`. La queja es que algo en esa suma no es un número, y la parte entre paréntesis te dice cuál: `field 'y'`, es decir, la `y` de dentro de una tabla. No tiene valor, porque nunca se lo diste, y algo sin valor en Lua es `nil`. Fíjate en dónde está el error y dónde está la equivocación. El error está en `updatePlaying`, a cuarenta líneas de `spawnBlock`, donde no hay nada mal en absoluto. `updatePlaying` es solo el primer trozo de código con la mala suerte de tocar el destrozo. Esto es lo normal, no la excepción, y seguir ese rastro hacia atrás es una habilidad. Vuelve a poner la línea. Luego pasa la página, porque la lección siguiente no trata de otra cosa. [#_what_you_learned_7] === Lo que has aprendido * Un juego es un mundo que cambia, un mundo que se dibuja y una decisión sobre cuál de las dos cosas hacer -- y son tres trozos de código separados. * Los callbacks de entrada deberían apuntar lo que está pulsado; la actualización debería decidir qué significa eso. * Una lista de registros es la forma de casi cualquier mundo de juego. * Quita cosas de una lista recorriéndola hacia atrás. * `collideRects` responde si dos rectángulos se solapan, y la versión del motor es mejor que la tuya porque es correcta, se lee bien y tiene una familia que volverás a ver. * Una variable `state` que guarda una palabra gana a tres indicadores de verdadero o falso, y les sigue ganando a medida que el juego crece. * Empieza una partida desde una función que lo reinicia todo, para que la segunda partida sea exactamente igual que la primera. * Los números que quizá quieras ajustar van en constantes con nombre al principio del archivo. * Construye un juego por etapas que funcionen cada una. Un programa que no ha funcionado nunca es mucho más difícil de arreglar que uno que funcionaba hace cinco minutos. [#_next_time_7] === La próxima vez Ya has escrito código suficiente para cometer errores interesantes. La lección siguiente es la que ningún libro para principiantes se molesta en incluir: qué dicen de verdad los mensajes de error de Singe, cómo averiguar lo que guarda realmente una variable en lugar de adivinarlo, y cómo partir un problema por la mitad hasta que no le quede ningún sitio donde esconderse.