[#_lesson_3_making_decisions] == Lección 3: Tomar decisiones image::learn/03-decisions.png[La lección terminada, 480] Todo lo que has escrito hasta ahora hace lo mismo en cada fotograma. La palabra de la lección dos se movía quisieras o no, y lo único que la hacía dar la vuelta eran cuatro bloques de código que tuviste que creerte sin más. Esta lección trata de esos cuatro bloques, como es debido. Al final de ella tu programa hará preguntas, actuará según las respuestas y -- como la pregunta más interesante que puede hacer un juego es «¿qué está haciendo el jugador?» -- estarás manejando una caja por la ventana con las teclas de flecha. Guarda esa caja. Es el jugador del juego que construyes en la lección siete. [#_start_a_new_folder_2] === Empieza una carpeta nueva Una carpeta llamada `decisions`, dentro un archivo llamado `decisions.singe`, y `Singe -R decisions` ejecutándose en una terminal. Igual que la última vez. [#_what_an_if_really_says] === Lo que dice de verdad un `if` Aquí está el bloque de la lección dos por separado. [source,lua] ---- if wordX > lastColumn then wordX = lastColumn end ---- Cuatro partes. La palabra `if`. Una pregunta. La palabra `then`. Y un cuerpo de líneas que solo se ejecutan cuando la respuesta a la pregunta es sí, cerrado con `end`. La pregunta es `wordX > lastColumn`, y el `>` es el que hace el trabajo de verdad. Compara los dos números que tiene a cada lado y da una respuesta que es *verdadero* o *falso*. Esas dos palabras no son texto ni son números; son valores por derecho propio, llamados *booleanos*, y un booleano es lo único que le interesa a un `if`. La sangría es la misma idea que en la lección uno: las líneas de dentro van sangradas para que veas de un vistazo qué pertenece al `if`, y el `end` se alinea con el `if` que lo abrió. Lua lo ejecutaría sin los espacios. Tú no podrías leerlo. Hay seis comparaciones como esta y las usarás todas. [cols="1,4"] |=== | `a < b` | a es menor que b | `a > b` | a es mayor que b | `a <= b` | a es menor o igual que b | `a >= b` | a es mayor o igual que b | `a == b` | a es lo mismo que b | `a ~= b` | a no es lo mismo que b |=== Las dos últimas son con las que hay que tener cuidado, por motivos distintos. `~=` significa «no es igual a». La mayoría de los lenguajes lo escriben `!=`, y si has visto algún programa antes, tus dedos escribirán `!=` y Lua lo rechazará. La virgulilla está en tu teclado junto al `1` o debajo del `#`, según dónde vivas. `==` son dos signos igual, y no es una errata por uno. Es uno de los errores más comunes en programación, y vale la pena pararse en él un momento. En la lección dos aprendiste que un signo igual significa *mete el lado derecho en el nombre de la izquierda*. Eso es algo que haces. Cambia algo. Dos signos igual significan *¿son iguales estas dos cosas?* Eso es una pregunta que haces. No cambia nada, y devuelve verdadero o falso. [source,lua] ---- lives = 3 if lives == 3 then ---- La primera línea pone `lives` a tres. La segunda pregunta si vale tres. Trabajos distintos, símbolos distintos, a un carácter de distancia. Lua te pillará si los confundes, y verás el error que usa al final de esta lección. [#_when_the_answer_is_no] === Cuando la respuesta es no Un `if` solo no hace nada en absoluto cuando la respuesta es falsa. Muchas veces quieres que en ese caso pase otra cosa, y eso es `else`. [source,lua] ---- if lives > 0 then message = "Ready" else message = "Game Over" end ---- Se ejecuta exactamente uno de esos dos cuerpos, nunca los dos, nunca ninguno, así que `message` siempre acaba guardando una de las dos cadenas y nunca nada. Cuando hay más de dos posibilidades, `elseif` las encadena. [source,lua] ---- if score >= 10000 then rank = "ace" elseif score >= 5000 then rank = "pilot" elseif score >= 1000 then rank = "rookie" else rank = "passenger" end ---- Lua prueba las preguntas desde arriba y se para en la primera que es verdadera. Una puntuación de 6000 no es mayor que 10000, así que prueba la siguiente, que es verdadera, así que `rank` pasa a ser `pilot` y las otras dos preguntas no se llegan a hacer. Eso último importa: el `elseif` de 1000 también es verdadero para una puntuación de 6000, y nunca se llega a él, y por eso el orden va de mayor a menor. Todo el bloque necesita exactamente un `end`, da igual cuántas partes `elseif` tenga. Es una sola decisión, no cuatro. [#_true_false_and_nothing_at_all] === Verdadero, falso y nada de nada No hace falta comparar dos cosas para obtener un booleano. Puedes guardar uno. [source,lua] ---- paused = false ---- Es una variable como cualquier otra, y el valor que tiene es el propio booleano `false`, escrito sin comillas porque no es la palabra «false», es la idea. Lo mismo pasa con `true`. Una variable que guarda uno de los dos puede ir directamente en un `if` sin ninguna comparación: [source,lua] ---- if paused then ---- Léelo como «si está en pausa». Se entiende mejor que `if paused == true`, que hace lo mismo dando un rodeo, y los programadores con experiencia te mirarán raro si lo escribes. Tres palabras unen y dan la vuelta a estas respuestas. [source,lua] ---- if alive and not shielded then if coins > 0 or freePlay then ---- `and` es verdadero cuando lo son los dos lados. `or` es verdadero cuando lo es cualquiera de los dos, o los dos. `not` convierte verdadero en falso y falso en verdadero. Se leen como el inglés y, cosa rara en programación, significan lo mismo que en inglés. Hay un valor más que conviene conocer, y es el que produce los errores más confusos de tu primer año. [source,lua] ---- overlayPrint(0, 0, "lives " .. tostring(lives)) ---- Si nunca has metido nada en `lives`, eso imprime `lives nil`. Lua no se queja de un nombre que no ha visto nunca; te da `nil`, que es su palabra para *aquí no hay nada*. No es cero y no es una cadena vacía. El cero es un número que querías guardar. `nil` es la ausencia de un valor. Ya te has encontrado con `nil` dos veces sin que nadie te lo dijera. `frames = frames + 1` sin la línea `frames = 0` encima falla porque le pediste a Lua que sumara uno a nada. Y un callback mal escrito también es `nil`, y por eso un `onOverlayUpdate` mal escrito te da una ventana negra y ningún error: Singe busca el nombre, no encuentra nada y sigue tan tranquilo con lo suyo. `tostring` está en esa línea porque `..` no une un booleano ni un `nil` con nada. Los números te los convierte; estos no. Esa era la promesa del final de la lección dos, y ahora tienes para qué usarlo. Por último, la regla que lo ata todo, porque `if` acepta cualquier cosa que le des, no solo verdadero y falso: [quote] En Lua todo es verdadero salvo `false` y `nil`. El cero es verdadero. Una cadena vacía es verdadera. Solo esos dos no lo son, y una vez que lo sabes, `if movingLeft then` se lee perfectamente tanto si `movingLeft` guarda `true` como `false`, o si nunca le has dado valor. [#_reading_the_controls] === Leer los controles Es hora de preguntarle algo al jugador. Para eso hace falta un callback nuevo. [source,lua] ---- function onInputPressed(what) if what == SWITCH_RIGHT then boxX = boxX + 10 end end ---- `onInputPressed` es el segundo callback que escribes, y funciona exactamente como describía la lección uno: tú lo escribes, Singe lo llama. A `onOverlayUpdate` se le llama con un temporizador. A este se le llama cuando pasa algo -- cuando el jugador pulsa cualquier cosa que el motor reconoce. La diferencia con `onOverlayUpdate` es la palabra que hay dentro de los paréntesis. Singe tiene que decirte *qué* control se ha pulsado, así que le da un valor a la función, y `what` es el nombre con el que has elegido recibirlo. La lección uno llamaba argumentos a las cosas que le pasas a una función; desde dentro, el nombre que recoge uno es un *parámetro*. El nombre es tuyo: llámalo `what`, llámalo `button`, llámalo `k`. Singe lo rellena igual. `SWITCH_RIGHT` es un nombre que el motor ya le ha dado a un valor, como `OVERLAY_UPDATED` en la lección uno. No tienes que cargar nada para usarlo; Singe los define todos antes de que se ejecute tu script. Hay uno para cada cosa que tiene una recreativa: `SWITCH_LEFT`, `SWITCH_RIGHT`, `SWITCH_UP`, `SWITCH_DOWN`, de `SWITCH_BUTTON1` a `SWITCH_BUTTON4`, `SWITCH_START1`, `SWITCH_COIN1` y más. La tabla de Constantes del motor de la referencia cubre toda la familia, que va de `SWITCH_UP` a `SWITCH_RECORD`. A propósito, no se llaman «la tecla de flecha derecha», porque llega el mismo `SWITCH_RIGHT` tanto si el jugador ha pulsado una tecla de flecha como si ha empujado un joystick o el stick de un mando. Escribes el juego una vez y funciona con los tres. Añade ese callback, dale a `boxX` un valor inicial fuera de las funciones y dibuja algo en `boxX`. Pulsa la flecha derecha. La caja salta diez píxeles cada vez que la pulsas. [#_it_happened_and_it_is_happening] === Ha pasado, y está pasando Mantén pulsada la tecla de flecha. No pasa nada más. A `onInputPressed` se le llama una vez, al bajar la tecla, y no otra vez hasta que la sueltas y la vuelves a pulsar. No hay repetición de teclas. Eso es justo lo correcto para algunas cosas -- disparar, echar una moneda, elegir una opción de un menú -- y justo lo incorrecto para caminar, que tiene que seguir pasando mientras la tecla esté pulsada. Son dos preguntas realmente distintas. *Ha pasado* es un evento, y te enteras una vez. *Está pasando* es un estado, y dura. Singe te avisa de los eventos, y la forma de convertir eventos en estado es recordarlos tú. Dos callbacks y una variable entre los dos. [source,lua] ---- movingRight = false function onInputPressed(what) if what == SWITCH_RIGHT then movingRight = true end end function onInputReleased(what) if what == SWITCH_RIGHT then movingRight = false end end ---- `onInputReleased` es el tercer callback, y es el reflejo del segundo: Singe lo llama cuando la tecla vuelve a subir, con el mismo valor `SWITCH_`. Entre los dos consiguen que `movingRight` diga la verdad sobre lo que está haciendo el dedo del jugador en este momento. Todavía no se ha movido nada, y de eso se trata. Pulsar y moverse son ahora dos trabajos separados. El movimiento va donde va todo el trabajo de cada fotograma. [source,lua] ---- function onOverlayUpdate() if movingRight then boxX = boxX + BOX_SPEED end ... ---- Ahora la caja se desliza suavemente mientras la tecla esté pulsada, a una velocidad que controlas tú, y la aritmética vuelve a estar en la única función que se ejecuta sesenta veces por segundo, que es donde debe estar. Esta forma -- los eventos ponen indicadores, el fotograma actúa según los indicadores -- es como casi todos los juegos manejan sus controles, y la volverás a usar en cada lección que tenga un jugador. [#_drawing_a_box] === Dibujar una caja `overlayBox` recibe dos esquinas: el borde izquierdo y el de arriba del rectángulo, y luego el derecho y el de abajo. [source,lua] ---- colorForeground(255, 220, 0) overlayBox(boxX, boxY, boxX + BOX_SIZE, boxY + BOX_SIZE) ---- Dibuja un *contorno*, cuatro líneas de un píxel de grosor, no un rectángulo relleno. En las llamadas de dibujo del overlay no hay ningún rectángulo relleno. Cuando más adelante quieras un bloque sólido hay formas de conseguirlo, y la lección nueve tiene la habitual. Sus números son píxeles, no celdas de carácter. Esto pilla a la gente, porque `overlayPrint` en la lección dos contaba en celdas de seis por trece píxeles y todo lo demás en Singe cuenta en píxeles. `overlayPrint` es el raro; el resto de las llamadas de dibujo -- `overlayBox`, `overlayLine`, `overlayCircle`, `overlayEllipse` y `overlayPlot` -- trabajan todas en los mismos píxeles que da `overlayGetWidth`. `colorForeground` pone el color, con tres números de 0 a 255: cuánto rojo, cuánto verde, cuánto azul. Los tres a 255 es blanco, todos a 0 es negro, y `(255, 220, 0)` es un amarillo cálido. Fíjate en que no es un argumento de `overlayBox`. Es un ajuste: lo cambias, y todo lo que dibujes después sale en el color nuevo hasta que lo vuelvas a cambiar. `overlayPrint` vuelve a ser la excepción -- la fuente de consola trae sus propios colores de serie y no te hace caso. [#_the_whole_thing_2] === Todo junto [source,lua] ---- BOX_SIZE = 20 BOX_SPEED = 2 width = overlayGetWidth() height = overlayGetHeight() maxX = width - BOX_SIZE - 1 maxY = height - BOX_SIZE - 1 boxX = width // 2 boxY = height // 2 movingLeft = false movingRight = false movingUp = false movingDown = false function onInputPressed(what) if what == SWITCH_LEFT then movingLeft = true elseif what == SWITCH_RIGHT then movingRight = true elseif what == SWITCH_UP then movingUp = true elseif what == SWITCH_DOWN then movingDown = true end end function onInputReleased(what) if what == SWITCH_LEFT then movingLeft = false elseif what == SWITCH_RIGHT then movingRight = false elseif what == SWITCH_UP then movingUp = false elseif what == SWITCH_DOWN then movingDown = false end end function onOverlayUpdate() if movingLeft and not movingRight then boxX = boxX - BOX_SPEED elseif movingRight and not movingLeft then boxX = boxX + BOX_SPEED end if movingUp and not movingDown then boxY = boxY - BOX_SPEED elseif movingDown and not movingUp then boxY = boxY + BOX_SPEED end if boxX < 0 then boxX = 0 elseif boxX > maxX then boxX = maxX end if boxY < 0 then boxY = 0 elseif boxY > maxY then boxY = maxY end overlayClear() if movingLeft or movingRight or movingUp or movingDown then colorForeground(255, 220, 0) else colorForeground(80, 160, 255) end overlayBox(boxX, boxY, boxX + BOX_SIZE, boxY + BOX_SIZE) overlayPrint(0, 0, "x " .. boxX .. " y " .. boxY) return OVERLAY_UPDATED end ---- Hay cuatro cosas ahí dentro que merecen una segunda mirada. `movingLeft and not movingRight` está ahí porque un jugador puede mantener pulsadas las dos flechas a la vez. Sin el `not`, las dos se pelearían y la caja temblaría o se iría hacia un lado; con él, si mantienes pulsadas las dos no gana ninguna y la caja se queda quieta, que es lo que espera un jugador. `maxX` se calcula una vez arriba, con `overlayGetWidth` como en la lección dos, y el `- 1` es porque el último píxel de la superficie es uno menos que su ancho. Quita el `- 1` y el borde derecho de la caja se sale de la superficie y desaparece, porque Singe tira sin quejarse todo lo que dibujas fuera del overlay. Los bloques que limitan la posición usan `elseif` en lugar de dos bloques `if` separados. La caja no puede salirse por el borde izquierdo y por el derecho a la vez, así que hacer la segunda pregunta cuando la primera era verdadera es trabajo perdido. Y la decisión del color es un `if` cuyo único trabajo es elegir un valor. Amarillo mientras el jugador está haciendo algo, azul mientras no, que es una forma gratis de ver que tu manejo de la entrada funciona de verdad. [#_try_it_3] === Pruébalo . *Cambia la velocidad.* `BOX_SPEED = 1` y `BOX_SPEED = 8`. Con ocho, fíjate en lo que hace el límite cuando empujas la caja contra una esquina. . *Muestra tus indicadores.* Añade una línea a `onOverlayUpdate`: `overlayPrint(0, 1, "left " .. tostring(movingLeft))`. Mantén pulsada y suelta la flecha izquierda y mira cómo cambia. Luego quita el `tostring` y lee el error que te da Lua. . *Que vaya más rápido con un botón pulsado.* Añade un indicador `running` controlado por `SWITCH_BUTTON1` en los dos callbacks de entrada, y usa un `if` en `onOverlayUpdate` para moverte el doble de `BOX_SPEED` mientras sea verdadero. . *Marca el centro.* Dibuja una segunda caja, pequeña, en el centro de la ventana, que cambie de color cuando la caja del jugador se solape con ella. Necesitarás `and` para unir cuatro comparaciones, y es más difícil de lo que parece. Que te salga un poco mal es normal; la lección once lo hace como es debido. . *Quita lo de soltar.* Borra toda la función `onInputReleased` y ejecútalo. La caja da un paso y luego no para nunca. Averigua qué línea no se alcanza nunca ahora. [#_break_it_on_purpose_3] === Rómpelo a propósito Busca el bloque que limita la posición y cambia el `<` de su primera comparación por un solo signo igual, para que quede así: [source,lua] ---- if boxX = 0 then ---- Guárdalo. La ventana se queda ahí vacía y la terminal dice: ---- Error running script: decisions/decisions.singe:57: 'then' expected near '=' ---- Este es el error del que avisaba toda la sección sobre `==`, y el mensaje es un buen ejemplo de un error que es totalmente exacto y totalmente inútil si te lo tomas al pie de la letra. Lua no te está pidiendo que añadas la palabra `then`. Lua llegó hasta `if boxX`, esperaba a continuación una comparación o un `then`, y se encontró `=`, que no puede ser ninguna de las dos cosas. Siempre que veas `'then' expected` después de un `if`, busca un `=` simple que debería ser doble. Casi siempre es eso. Fíjate en dónde ha aparecido este: el juego ni siquiera llegó a empezar, igual que con la cadena sin terminar de la lección uno, porque Lua no podía leer el archivo. El error del contador de fotogramas de la lección dos pasó durante la ejecución. Los errores en la forma de tu código te paran antes de que empiece el juego. Los errores en lo que hace tu código esperan a que se ejecute. [#_what_you_learned_3] === Lo que has aprendido * `if question then ... end` ejecuta un cuerpo solo cuando la respuesta es verdadera. * `else` da el otro caso, y `elseif` encadena más; gana la primera pregunta verdadera y las demás no se llegan a hacer. * Las comparaciones son `<`, `>`, `<=`, `>=`, `==` y `~=`. * `=` mete un valor en un nombre. `==` pregunta si dos cosas son iguales. * `true` y `false` son valores, y puedes guardarlos en variables. * `and`, `or` y `not` unen y dan la vuelta a esas respuestas. * `nil` es lo que obtienes de un nombre al que nunca has dado valor, y no es cero. * Todo es verdadero salvo `false` y `nil`. * `onInputPressed` y `onInputReleased` son callbacks que Singe llama con un valor `SWITCH_` cuando el jugador pulsa o suelta un control. * Una pulsación es un evento y ocurre una vez; guarda el estado en una variable y actúa según él en cada fotograma. * `overlayBox` dibuja un contorno en píxeles, y `colorForeground` es un ajuste, no un argumento. [#_next_time_3] === La próxima vez Una caja es fácil. Cien estrellas no lo son, al menos no tal como has escrito las cosas hasta ahora, porque escribir cien líneas `overlayPlot` no es el plan ideal para una tarde. La lección cuatro trata del bucle: decirle al ordenador que haga lo mismo muchas veces, con algo distinto en cada vuelta, que es lo que hace posible llenar una pantalla de cualquier cosa.